#
# "@(#) $Id: Track.py,v 1.1.1.1 2006-07-27 03:16:08 gioshe Exp $"
#
# This work is released under the GNU GPL, version 2 or later.
#
from Utils.kdeemul import *
from Utils.musicgenres import *
import time, os, re
from Player import *
from Utils.utils import *
import base64,traceback
import Utils.crushToAscii

_trackProperties = {
	'album':[True,True,['unicode','str'],None],
	'artist':[True,True,['unicode','str'],None],
	'band':[True,True,['unicode','str'],None],
	'bitRate':[True,True,['int','long'],None],
	'bpm':[True,True,['int','long'],None],
	'channels':[True,True,['int','long'],None],
	'comment':[True,True,['unicode','str'],None],
	'compilation':[True,True,'bool',None],
	'composer':[True,True,['unicode','str'],None],
	'conductor':[True,True,['unicode','str'],None],
	'contentGroup':[True,True,['unicode','str'],None],
	'contentType':[True,True,['unicode','str'],None],
	'copyright':[True,True,['unicode','str'],None],
	'dateAdded':[True,True,'struct_time',None],
	'dateEncoded':[True,True,'struct_time',None],
	'dateModified':[True,True,'struct_time',None],
	'description':[True,True,['str','unicode'],None],
	'discCount':[True,True,['int','long'],None],
	'discNum':[True,True,['int','long'],None],
	'enabled':[True,True,'bool',True],
	'encodedBy':[True,True,['unicode','str'],None],
	'encoderSettings':[True,True,['unicode','str'],None],
	'endTime':[True,True,['int','long'],None],
	'fileCreator':[True,True,['unicode','str'],None],
	'fileType':[True,True,['unicode','str'],None],
	'format':[True,True,['unicode','str'],None],
	'grouping':[True,True,['unicode','str'],None],
	'id3Version':[True,True,['unicode','str'],None],
	'initialKey':[True,True,['unicode','str'],None],
	'involvedPeople':[True,True,['unicode','str'],None],
	'isCD':[True,True,'bool',False],
	'iSrc':[True,True,['unicode','str'],None],
	'isPreview':[True,True,'bool',False],
	'isStream':[True,True,'bool',False],
	'kind':[True,True,['unicode','str'],None],
	'language':[True,True,['unicode','str'],None],
	'location':[True,True,['unicode','str'],None],
	'lsongs':[True,True,'bool',False],
	'lyricist':[True,True,['unicode','str'],None],
	'lyrics':[True, True,['unicode','str'],None],
	'managed':[True,True,'bool',False],
	'mediaType':[True,True,['unicode','str'],None],
	'mixArtist':[True,True,['unicode','str'],None],
	'mode':[True,True,['unicode','str'],None],
	'netRadioOwner':[True,True,['unicode','str'],None],
	'netRadioStation':[True,True,['unicode','str'],None],
	'originalLocation':[True,True,['unicode','str'],None],
	'origAlbum':[True,True,['unicode','str'],None],
	'origArtist':[True,True,['unicode','str'],None],
	'origFileName':[True,True,['unicode','str'],None],
	'origLyricist':[True,True,['unicode','str'],None],
	'origYear':[True,True,'int',None],
	'origAlbum':[True,True,['unicode','str'],None],
	'playCount':[True,True,['int','long'],0],
	'playDate':[True,True,'struct_time',None],
	'player':[True,True,'bool',False],
	'playlistDelay':[True,True,['int','long'],None],
	'playStatus':[True,True,'str','idle'],
	'preset':[True,True,'str','None'],
	'publisher':[True,True,['unicode','str'],None],
	'purchased':[True,True,'bool',None],
	'rating':[True,True,'int',0],
	'recordingDates':[True,True,'str',None],
	'remote':[True,True,'bool',False],
	'ripStatus':[True,True,'str','idle'],
	'sampleRate':[True,True,['int','long'],None],
	'shortName':[True,True,['unicode','str'],None],
	'size':[True,True,['int','long'],0],
	'songLen':[True,True,['int','long'],None],
	'startTime':[True,True,['int','long'],None],
	'subTitle':[True,True,['unicode','str'],None],
	'totalTime':[True,True,['int','long'],None],
	'title':[True,True,['unicode','str'],None],
	'trackCount':[True,True,['int','long'],None],
	'trackID':[True,True,['int','long'],None],
	'trackNum':[True,True,['int','long'],None],
	'volume':[True,True,'int',0],
	'wavPath':[True,True,['unicode','str'],None],
	'wwwartist':[True,True,['unicode','str'],None],
	'wwwaudiofile':[True,True,['unicode','str'],None],
	'wwwaudiosource':[True,True,['unicode','str'],None],
	'wwwcommercialinfo':[True,True,['unicode','str'],None],
	'wwwcopyright':[True,True,['unicode','str'],None],
	'wwwpayment':[True,True,['unicode','str'],None],
	'wwwpublisher':[True,True,['unicode','str'],None],
	'wwwradiopage':[True,True,['unicode','str'],None],
	'year':[True,True,'int',None]
}

def _compareTypes(value,types):
	valueType = type(value).__name__
	if type(types).__name__=='str':
		return valueType==types
	else:
		for i in types:
			if valueType==i:
				return True
		return False

def _assign(aDict,aName,aValue):
	if aValue!=None:
		aDict[aName] = aValue

def _get(a,b):
	if a==None: return b
	return a

class Track(object):
	def __init__(self,library = None):
		self._qObject = QObject()
		# create my internal special dictionary
		self.__dict__['_Track__mydict'] = {}
		self._library = library

	def __getstate__(self):
		return [self.__dict__['_Track__mydict'],self._library]
	
	def __setstate__(self,stuff):
		(self.__dict__['_Track__mydict'],self._library) = stuff
		self._qObject = QObject()
	
	def qObject(self):
		return self._qObject

	def emit(self,signal,arguments):
		self._qObject.emit(signal,arguments)

	def connect(self,signal,target):
		QObject.connect(self._qObject,signal,target)

	def __getattr__(self,name):
		try:
			# see if the attribute is in my special list
			# if so, return current value, or default
			t = _trackProperties[name]
			if t[0]:
				try:
					return self.__mydict[name]
				except:
					return t[3]
		except KeyError:
			# not in my list, so see if it's special
			# if so, raise error, otherwise return normal attribute
			if name.startswith('__') and name.endswith('__'):
				raise AttributeError,name
			else:
				return self.__dict__[name]

	def __setattr__(self,name,value):
		try:
			# see if the attribute is in the special list.
			# if so, enforce the type and assign
			# if the value is None, remove the attribute
			t = _trackProperties[name]
			if t[1]:
				if value==None:
					try:    del self.__mydict[name]
					except: pass
				elif _compareTypes(value,t[2]):
					try: oldValue = self.__mydict[name]
					except: oldValue = None
					self.__mydict[name] = value
					if oldValue!=value:
						self.emit(PYSIGNAL('changedAttribute'),(self,name,oldValue,value))
						if self._library!=None:
							self._library.changedTrackAttribute(self,name,oldValue,value)
				else:
					raise AttributeError, "value '"+str(value)+"' is wrong type ("+type(value).__name__+") for "+name
			else:
				raise AttributeError,"Attribute "+name+" is read-only"
		except KeyError:
			# not in the list, so check if it's a property
			# if so, call the appropriate setter
			# otherwise assign normally
			for v in self.__class__.__mro__:
				p = vars(v).get(name,False)
				if p:
					if type(p).__name__=='property':
						p.__set__(self,value)
						return
					else:
						raise AttributeError,"Can't set"+name+" attribute"
			self.__dict__[name] = value

	def __delattr__(self,name):
		# try to remove from special list, otherwise from normal list
		try:
			del self.__mydict[name]
		except KeyError:
			del self.__dict__[name]				

	def get(name,default):
		return self.__dict(name,default)

	def setGenre(self,value):
		#print "setting genre to",value
		self.contentType = "("+str(musicGenreToIndex(value))+")"
		#print "result is",self.contentType
	def getGenre(self):
		try:    return musicGenreFromIndex(eval(self.contentType))
		except: return None
	genre = property(getGenre,setGenre)

	def getPList(self):
		track = {}
		_assign(track,"Track ID",self.trackID)
		_assign(track,"Name",self.title)
		_assign(track,"Artist",self.artist)
		_assign(track,"Album",self.album)
		_assign(track,"Kind",self.kind)
		_assign(track,"Size",self.size)
		_assign(track,"Channels",self.channels)
		_assign(track,"ID3 Version",self.id3Version)
		_assign(track,"Mode",self.mode)
		_assign(track,"Total Time",self.totalTime)
		_assign(track,"Track Number",self.trackNum)
		_assign(track,"Track Count",self.trackCount)
		_assign(track,"Disc Number",self.discNum)
		_assign(track,"Disc Count",self.discCount)
		_assign(track,"Date Encoded",self.dateEncoded)
		_assign(track,"Date Modified",self.dateModified)
		_assign(track,"Date Added",self.dateAdded)
		_assign(track,"Bit Rate",self.bitRate)
		_assign(track,"Sample Rate",self.sampleRate)
		_assign(track,"Format",self.format)
		_assign(track,"Play Count",self.playCount)
		_assign(track,"Play Date",self.playDate)
		_assign(track,"File Type",self.fileType)
		_assign(track,"File Creator",self.fileCreator)
		_assign(track,"Location",self.location) #unicode(self.location,'ISO-8859-1'))
		_assign(track,"Original Location",self.originalLocation)
		_assign(track,"Genre",self.genre)
		_assign(track,"Rating",self.rating)
		_assign(track,"Composer",self.composer)
		_assign(track,"Year",self.year)
		_assign(track,"Encoded By",self.encodedBy)
		_assign(track,"Comment",self.comment)
		_assign(track,"Volume",self.volume)
		_assign(track,"BPM",self.bpm)
		_assign(track,"Grouping",self.grouping)
		_assign(track,"Enabled",self.enabled)
		_assign(track,"Managed",self.managed)
		_assign(track,"Compilation",self.compilation)
		_assign(track,"Short Name",self.shortName)
		_assign(track,"Purchased",self.purchased)
		_assign(track,"Lyrics",self.lyrics)
		return track

	def setPList(self,track):
		#print "parsing Track from ",track
		self.trackID = track['Track ID']
		try: self.title = track['Name']
		except: pass
		try: self.artist = track['Artist']
		except: pass
		try: self.album = track['Album']
		except: pass
		try: self.kind = track['Kind']
		except: pass
		try: self.format = track['Format']
		except: pass
		self.size = track.get('Size',None)
		self.channels = track.get('Channels',None)
		self.mode = track.get('Mode',None)
		self.id3Version = track.get('ID3 Version',None)
		self.totalTime = track.get('Total Time',None)
		self.trackNum = track.get('Track Number',None)
		self.trackCount = track.get('Track Count',None)
		self.discNum = track.get('Disc Number',None)
		self.discCount = track.get('Disc Count',None)
		try: self.dateModified = track.get('Date Modified',None)
		except: pass
		try: self.dateAdded = track.get('Date Added',None)
		except: pass
		try: self.dateEncoded = track.get('Date Encoded',None)
		except: pass
		try: self.dateModified = track.get('Date Modified',None)
		except: pass
		self.bitRate = track.get('Bit Rate',None)
		self.sampleRate = track.get('Sample Rate',None)
		self.playCount = track.get('Play Count',None)
		try: self.playDate = track.get('Play Date',None)
		except: pass
		self.managed = track.get('Managed',True) # XXX DSM handle old databases
		try: self.fileType = track['File Type']
		except: pass
		try: self.fileCreator = track['File Creator']
		except: pass
		self.location = unicode(track['Location']).encode('ISO-8859-1')
		#try: self.location = unicode(track['Location']).encode('ISO-8859-1')
		#except: pass
		try: self.originalLocation = track['Original Location']
		except: pass
		try: self.genre = track['Genre']
		except: pass
		self.rating = track.get('Rating',None)
		try: self.composer = track['Composer']
		except: pass
		self.year = track.get('Year',None)
		try: self.encodedBy = track['Encoded By']
		except: pass
		try: self.comment = track['Comment']
		except: pass
		self.volume = track.get('Volume',None)
		self.bpm = track.get('BPM',None)
		try: self.grouping = track['Grouping']
		except: pass
		try: self.enabled = track.get('Enabled')
		except: pass
		try: self.compilation = track.get('Compilation')
		except: pass
		try: self.shortName = track.get('Short Name')
		except: pass
		try: self.purchased = track.get('Purchased')
		except: pass
		try: self.lyrics = track.get("Lyrics")
		except: pass
	pList = property(getPList,setPList)

	def getRemotePList(self):
		p = self.pList
		shortName = os.path.split(p['Location'])[1]
		p['Short Name'] = shortName
		try: del p['Location']
		except: pass
		try: del p['Original Location']
		except: pass
		return p
	remotePList = property(getRemotePList)

	def play(self):
		raise NotImplementedError,"subclass responsibility"

	#
	# update the track info, typically from user edit
	#
	def loadTagsFrom(self,tags):
		if tags==None or len(tags)==0: return
		try: self.artist = tags['artist']
		except: pass
		try: self.album = tags['album']
		except: pass
		try: self.title = tags['title']
		except: pass
		try: self.year = int(tags['year'])
		except: pass
		try: self.rating = int(tags['rating'])
		except: pass
		try: self.composer = tags['composer']
		except: pass
		try: self.grouping = tags['grouping']
		except: pass
		try: self.trackNum = int(tags['trackNum'])
		except: pass
		try: self.trackCount = int(tags['trackCount'])
		except: pass
		try: self.discNum = int(tags['discNum'])
		except: pass
		try: self.discCount = int(tags['discCount'])
		except: pass
		try: self.genre = tags['genre']
		except: pass
		try: self.comment = tags['comment']
		except: pass
		try: self.bpm = int(tags['bpm'])
		except: pass
		try: self.compilation = tags['compilation']
		except: pass
		try: self.image=tags['image']
		except: pass
		try: self.lyrics = tags['lyrics']
		except: pass
		self.dateModified = time.gmtime(time.time())
		

	def loadTagsInto(self,tags):
		if tags==None:
			tags = {}
			if self.title: tags['title'] = self.title
			if self.artist: tags['artist'] = self.artist
			if self.album: tags['album'] = self.album
			if self.grouping: tags['grouping'] = self.grouping
			if self.composer: tags['composer'] = self.composer
			if self.comment: tags['comment'] = self.comment
			if self.genre: tags['genre'] = self.genre
			if self.year: tags['year'] = self.year
			if self.rating: tags['rating'] = self.rating
			if self.trackNum: tags['trackNum'] = self.trackNum
			if self.trackCount: tags['trackCount'] = self.trackCount
			if self.discNum: tags['discNum'] = self.discNum
			if self.discCount: tags['discCount'] = self.discCount
			if self.compilation: tags['compilation'] = self.compilation
		elif len(tags)>0:
			try:
				if tags['title']!=self.title: del tags['title']
			except: pass
			try:
				if tags['artist']!=self.artist: del tags['artist']
			except: pass
			try:
				if tags['album']!=self.album: del tags['album']
			except: pass
			try:
				if tags['grouping']!=self.grouping: del tags['grouping']
			except: pass
			try:
				if tags['composer']!=self.composer: del tags['composer']
			except: pass
			try:
				if tags['comment']!=self.comment: del tags['comment']
			except: pass
			try:
				if tags['genre']!=self.genre: del tags['genre']
			except: pass
			try:
				if tags['year']!=self.year: del tags['year']
			except: pass
			try:
				if tags['rating']!=self.rating: del tags['rating']
			except: pass
			try:
				if tags['trackNum']!=self.trackNum: del tags['trackNum']
			except: pass
			try:
				if tags['trackCount']!=self.trackCount: del tags['trackCount']
			except: pass
			try:
				if tags['discNum']!=self.discNum: del tags['discNum']
			except: pass
			try:
				if tags['discCount']!=self.discCount: del tags['discCount']
			except: pass
			try:
				if tags['compilation']!=self.complication: del tags['compilation']
			except: pass
			# XXX DSM More?
		#print "loaded",self.title,"result is",tags
		return tags

	def getInfo(self,parent=None):
		from UI.GetInfoDialog import GetInfoDialog
		#from UI.GetInfoPanel import GetInfoPanel
		getInfoDialog = GetInfoDialog(self)
		#getInfoDialog = GetInfoPanel(parent,self)
		ret = getInfoDialog.exec_loop()
		if ret==QDialog.Accepted:
			tags = getInfoDialog.unload()
			self.loadTagsFrom(tags)

	def showLocation(self):
		(path,file) = os.path.split(self.location)
		os.system("konqueror \"%s\" &" % path)

	def hashKey(self):
		try: 
			if self._hash:
				return self._hash
		except:
			pass
		s = "%s_%s_%s" % (crushToAscii.crush(self.title).strip(),crushToAscii.crush(_get(self.artist,"")).strip(),crushToAscii.crush(_get(self.album,"")).strip())
		if type(s).__name__=="unicode":
			s = s.encode("latin-1")
		self._hash =  s.strip()
		return self._hash
	
	def parseInfoFromName(self,name):
		artistTitle = re.compile(r'^(.+?) \- (.+?)$')
		mo = artistTitle.match(name)
		if mo: return (unikode(mo.group(1)),unikode(mo.group(2)))
		else: return (None,unikode(name))
	
	def titleKey(self):
		return _get(self.title,"<Unknown Title>").upper()
	
	def artistKey(self):
		artist = self.artist
		if artist:
			for article in ["The ", "El ", "La ", "Los ", "Las ", "Le ", "Les "]:
				alen = len(article)
				if artist[0:alen]==article:
					artist = artist[alen:]
					break
			return artist.upper()
		else: return "ZZZZZZZZZZZZZZZ"
	
	def albumKey(self):
		album = self.album
		if album: return album.upper()
		else: return "ZZZZZZZZZZZZZZZ"

	def durationKey(self):
		if self.totalTime: return "%010d" % _get(self.totalTime,0)
		return "ZZZ"
	
	def discTrackKey(self):
		discNum = _get(self.discNum,0)
		trackNum = _get(self.trackNum,0)
		return ' D%05dT%05d' % (discNum,trackNum)

	def genreKey(self):
		return _get(self.genre,"ZZZZ")

	def staticFromPList(pList,library=None):
		t = Track(library)
		t.pList = pList
		return t

	fromPList = staticmethod(staticFromPList)
	
	def canonicalArtist(self):
		try: return self.artist.upper()
		except: return self.artist
	
	def canonicalAlbum(self):
		try: return self.album.upper()
		except: return self.album
	
	def cleanup(self):
		self.playStatus = 'idle'
		self._hash = None

class FileTrack(Track):
	def __init__(self,library = None):
		#if library==None:
		#	raise ValueError,"must supply valid library"
		Track.__init__(self,library)
		self.connect(PYSIGNAL("changedAttribute"),self.fixFiles)

	#
	# add this track to a library, making a copy of the original
	# file into the appropriate artist and album directories,
	# building them as necessary
	#
	def addToLibrary(self,library):
		self._library = library
		self.trackID = library.uniqueTrackID()
		if self.managed:
			(srcPath,fileName) = os.path.split(self.location)
			destPath = self._library.makePathFor(self.artist,self.album)
			fileName= self._library.copyFile(fileName,srcPath,destPath)
			self.location = os.path.join(destPath,fileName)
			#print "Track copying from",srcPath,"to",self.location
		self.dateAdded = time.gmtime(time.time())
		library.addTrack(self)
	
	def setFile(self,filePath,thread = None):
		self.location = filePath
		(srcPath,fileName) = os.path.split(filePath)
		(rootName,ext) = os.path.splitext(fileName)
		(artist,title) = self.parseInfoFromName(rootName)
		if artist: self.artist = artist
		if title: self.title = title
		stats = os.stat(filePath)
		self.size = stats[6]
	file = property(None,setFile)

	def getShortName(self):
		(srcPath,fileName) = os.path.split(self.location)
		return fileName

	#
	# fetch the (first) artwork for this track (returns raw data, not pixmap)
	#
	def getImage(self):
		location = self.location
		try:
			(path,file) = os.path.split(location)
			for name in ['Folder.jpg']: # XXX DSM add other possible names for art
				fileName = os.path.join(path,name)
				#print "Track.getImage: trying",fileName
				if os.path.exists(fileName):
					return open(fileName).read()
			return None
		except: return None
	def setImage(self,image):
		pass
	image = property(getImage,setImage)

	#
	# play this track
	#
	def play(self,playlist=None):
		if os.path.exists(self.location):
			from Player import Player
			Player.playTrack(self,self._library,playlist)
		else:
			KMessageBox.error(None,i18n("The file for this track is missing"),i18n("Missing file"))
	
	def pause(self):
		Player.pause()

	#
	# if the artist or album changes
	# move the file, resolving any name conflicts
	# if the old album directory is empty, remove it
	# if the artist directory is now empty, remove it too
	#
	def fixFiles(self,track,name,oldValue,newValue):
		if self.managed and self.location and self._library:
			if (name=='album' or name=='artist') and oldValue!=newValue:
				(oldPath,fileName) = os.path.split(self.location)
				newPath = self._library.makePathFor(self.artist,self.album)
				#print "FileTrack needs to move",fileName,"from",oldPath,"to",newPath
				fileName = self._library.moveFile(fileName,oldPath,newPath)
				self.location = os.path.join(newPath,fileName)
				self._library.removeIfEmpty(oldPath)
	
	#
	# remove the file associated with this track
	# remove the album and artist directories if possible
	#
	def kill(self):
		#print "attempting to delete",self.title,"at",self.location
		if self.managed:
			#print "unmanaged, deleting"
			(path,fileName) = os.path.split(self.location)
			try: os.remove(self.location)
			except: pass
			self._library.removeIfEmpty(path)
	
	def data(self):
		return open(self.location).read()
	
	def encodedData(self):
		return base64.encodestring(self.data())

stream = None # persistent stream to work around bug in xine_dispose()

class XineTrack(FileTrack):
	def __init__(self,library=None):
		FileTrack.__init__(self,library)

	def cleanPath(self,filePath):
		return re.sub(r"#",r"%23",filePath)

	def setFile(self,filePath,thread = None):
		FileTrack.setFile(self,filePath,thread)
		global stream
		if stream==None:
			xine = Player.xine()
			ao = xine.open_audio_driver(LsongsDummyAudioDevice)
			stream = xine.stream_new(ao)
		stream.open(self.cleanPath(filePath))
		if thread: thread.msleep(100)
		self.bitRate = stream.bitrate
		self.sampleRate = stream.audio_samplerate
		(pos,cur,total) = stream.get_pos_length()
		self.totalTime = long(total*1000)
		self.channels = stream.audio_channels
		self.mode = stream.audiocodec
		stream.close()

if __name__=='__main__':
	t = Track.fromPList({'Track ID':33,'Name':'Some Song','Artist':'Some Artist','Album':'Some Album'})
	t.album = "Some Other Album"
	t.genre = "Rock"
	print t.genre
	print t.pList
